'''
Created on Jun 12, 2011

@author: Earl Bell
'''
from Tkinter import *
from OSEAboutDialog import *
from OSEMicroSettingsDialog import OSEMicroSettingsDialog
from OSESerialComs import *
import tkMessageBox
import tkSimpleDialog
from OSEThermocouple import *
from OSEEncoder import *
from OSEPhotogate import *
import OSEKitsDefine
import tkFileDialog
import time
import datetime
from OSEKitsDefine import THERMOCOUPLE_MODE

class OSEMainWindow():
    '''
    Class: OSEMainWindow
    
    Description:
        This class is the main controlling class for the OSEkits Experiment Control Software.
        All main functionality and control is handled by this class.
    '''    
    
    root_window = Tk()
    serialComs = OSESerialComs()
    __measurementFlag = 2
    _measurementThread = threading.Thread()
    _thermocouple = OSEThermocouple()
    _encoder = OSEEncoder()
    _photogate = OSEPhotogate()
    __sensorMode = OSEKitsDefine.THERMOCOUPLE_MODE
    _microcontoller_connected = 0
    
    def __init__(self):
        '''
        Constructor
        '''

        
        self.root_window.title("OSEKits Experiment Control Software")
        self._createMenus()
        self.root_window.protocol("WM_DELETE_WINDOW", self._terminate)
        self._startstopButton = Button(self.root_window, text = "Start", command = self._startMeasurement)
        
        if self.__sensorMode == OSEKitsDefine.THERMOCOUPLE_MODE:
            self._thermocouple.setup_display(self.root_window)
        elif self.__sensorMode == OSEKitsDefine.ENCODER_MODE:
            self._encoder.setup_display(self.root_window)
        elif self.__sensorMode == OSEKitsDefine.PHOTOGATE_MODE:
            self._photogate.setup_display(self.root_window, self._startstopButton)
        
        self._startstopButton.grid(column = 0, row = OSEKitsDefine.CONTROL_ROW, sticky =S+W)
        
        self._measurementThread = threading.Thread(target=self._measurement_handling).start()
        self._measurementtimingthread = threading.Thread(target=self._measurement_timing_thread).start()
    
        return
    
    def _measurement_handling(self):
        '''
        Function: _measurement_handling
        
        Description:
            This function runs as a thread.
            _measurement_handling is started when the main process starts. Control of the thread is done
            with the __measurementFlag variable.  As long as it is greater than zero the thread keeps cycling
            in a while loop.  When __measurementFlag is equal to 1 the thread actively polls the serialComs object's
            isMeasurementValueReady function to check for new measurement data. If there is that data is handled, if
            not the thread keeps polling.  If __measurementFlag is equal to 3 this thread polls isMeasurementValueReady
            until it returns False. This is done to clear out any remaining measurements before returning to a wait
            state.
            
        Returns:
            Nothing
            
        '''
        while self.__measurementFlag > 0:

            while self.__measurementFlag == 1:
                if self.serialComs.isMeasurementValueReady():
                    measurementData = self.serialComs.getMeasurementValue()
                    
                    if self.__sensorMode == OSEKitsDefine.THERMOCOUPLE_MODE:
                        self._thermocouple.add_measurement(measurementData) 
                    elif self.__sensorMode == OSEKitsDefine.ENCODER_MODE:
                        self._encoder.add_measurement(measurementData)
                    elif self.__sensorMode == OSEKitsDefine.PHOTOGATE_MODE:
                        self._photogate.add_measurement(measurementData)
                        
                if len(self.serialComs._received_command_packets) > 0:
                    for command in self.serialComs._received_command_packets:
                        if (command >> 6) == OSEKitsDefine.ACK_DONE:
                            self.serialComs._received_command_packets.remove(command)
                            if self.__sensorMode <> OSEKitsDefine.THERMOCOUPLE_MODE:
                                self._stopMeasurement()
                            
                                    
            if self.__measurementFlag == 3:
                sleep(1)
                while self.serialComs.isMeasurementValueReady():
                    measurementData = self.serialComs.getMeasurementValue()
                    if self.__sensorMode == OSEKitsDefine.THERMOCOUPLE_MODE:
                        self._thermocouple.add_measurement(measurementData) 
                    elif self.__sensorMode == OSEKitsDefine.ENCODER_MODE:
                        self._encoder.add_measurement(measurementData)
                    elif self.__sensorMode == OSEKitsDefine.PHOTOGATE_MODE:
                        self._photogate.add_measurement(measurementData)
                self.__measurementFlag = 2
                
                if self.__sensorMode == OSEKitsDefine.THERMOCOUPLE_MODE:
                    self._thermocouple.stop_measurement()
                elif self.__sensorMode == OSEKitsDefine.ENCODER_MODE:
                    self._encoder.stop_measurement()
                elif self.__sensorMode == OSEKitsDefine.PHOTOGATE_MODE:
                    self._photogate.stop_measurement()
        return
    
    def _measurement_timing_thread(self):
        '''
        Function: _measurement_timing_thread
        
        Description:
            This function is run as a thread.  Control of the thread is handled by the __measurementFlag variable.
            If __measurementFlag is greater than zero this thread keeps running.
            If __measurementFlag is equal to 1 and __sensorMode is equal to a sensor that needs this thread then
                this thread sends a control packet out once every time period set by the class for that sensor mode.
            If __measurementFlag is anything else greater than zero it polls __measurementFlag
        
        Returns:
            Nothing
        '''
        last_measurement_time = time.time()
        while self.__measurementFlag > 0:
            while self.__measurementFlag == 1 and self.__sensorMode == OSEKitsDefine.THERMOCOUPLE_MODE:
                
                if time.time() > (last_measurement_time + self._thermocouple.get_sample_rate()):
                    result = self.serialComs.sendControlPacket(OSEKitsDefine.CONTROL_START)
                    if result == "success":
                        last_measurement_time = time.time()
                    else:
                        tkMessageBox.showerror("Acknowledge Error", "A bad acknowledge was sent from the micro-controller.\n"+ str(result))
        return
        
                    
    def _startMeasurement(self):
        '''
        Function: _startMeasurement
        
        Description:
            This function is called when an experiment is to be started.  It sends config and control packets to 
            the micro-controller telling it to start taking measurements.
            
        Returns:
            Nothing
        
        '''
        #send configuration data to micro-controller for the current sensor type
        result = self._reconfigure_microcontroller()
        if result <> "success":
            tkMessageBox.showerror("Micro-controller Error", "There was an error configuring the micro-controller.\n"+str(result))
            return
        
        #send the start control packet to the micro-controller and call the start_measurement routine for 
        #the appropriate sensor type 
        result = self.serialComs.sendControlPacket(OSEKitsDefine.CONTROL_START)
        
        if result == "success":
            if self.__sensorMode == OSEKitsDefine.THERMOCOUPLE_MODE:
                result = self._thermocouple.start_measurement()
            elif self.__sensorMode == OSEKitsDefine.ENCODER_MODE:
                result = self._encoder.start_measurement()
            elif self.__sensorMode == OSEKitsDefine.PHOTOGATE_MODE:
                result = self._photogate.start_measurement()
            
            
            if result == "success":                        
                self.__measurementFlag = 1
                self._startstopButton.configure(text = "STOP", command = self._stopMeasurement)
                self._startstopButton.update()
            else:
                
                result = self.serialComs.sendControlPacket(OSEKitsDefine.CONTROL_STOP)
                if result <> "success":
                    tkMessageBox.showerror("Serial Acknowledge error.", "The micro-controller returned a bad acknowledge")
                
        elif result == "acknowledge error":
            tkMessageBox.showerror("Serial Acknowledge error.", "The micro-controller returned a bad acknowledge")
        elif result == "SERIAL ERROR: write failed":
            self._disconnectMicro()
            
        else:
            tkMessageBox.showerror("Connection Error", "There was an unknown error with the serial connection.\n Shutting down serial connection.  Try connecting to micro-controller again.")
            self._disconnectMicro()
            
    def _stopMeasurement(self):
        '''
        Function: _stopMeasurement
        
        Description:
            This function is called to stop the system from taking measurements.  A control packet is sent to the
            micro-controller and all flags are set to put the system back into normal operation.
        
        Returns:
            Nothing
        '''
        
        self.__measurementFlag = 3
        result = self.serialComs.sendControlPacket(OSEKitsDefine.CONTROL_STOP)
     
        if result == "success":
            sleep(1)
            self._startstopButton.configure(text = "START", command = self._startMeasurement)
            self._startstopButton.update()
            
        else:
            tkMessageBox.showerror("Serial Acknowledge error.", "The microcontroller returned a bad acknowledge")
        
    
    def _microControllerSettings(self):
        '''
        Function: _microControllerSettings
        
        Description:
            Creates a dialog for altering the settings for the communication with the
            Arduino Uno micro-controller.
        
        Returns: 
            Nothing
        '''
        mircosettings_dialog = OSEMicroSettingsDialog(self.root_window, self.serialComs)
        self.root_window.wait_window(mircosettings_dialog.settings_window)

        return
    
    
    def _terminate(self):
        '''
        Function: _terminate
        
        Description:
            This function terminates the program in a safe manner.  Sends a stop control packet to
            the micro-controller and shuts down all threads running for the GUI.
            
        Returns:
            Nothing
        '''
    
        if self._microcontoller_connected == 1:
            result = self.serialComs.sendControlPacket(OSEKitsDefine.CONTROL_STOP)
        result = self.serialComs.closePort()

        self.__measurementFlag = 0
        while (self._measurementThread <> None) and (self._measurementThread.isAlive()):
            wait = 1
 

        
        while(self._measurementtimingthread <> None) and (self._measurementtimingthread.isAlive()): 
            wait = 1

        
        self.root_window.quit()
        exit()
        
    
    def _aboutDisplay(self):
        '''
        Function: _aboutDisplay

        Description:
            Pops up a dialog window of class OSEAboutDialog, which displays the "About" 
            information for this program.
        
        Returns:
            Nothing
        '''
        about_dialog = OSEAboutDialog(self.root_window)
        self.root_window.wait_window(about_dialog.about_window)
        
        return
    
    def _connectMicro(self):
        '''
        Function: _connectMicro
        
        Description:
            Initializes all serial communication and sends config packet to the micro-controller.
        
        Returns:
            Nothing
        '''
        result = self.serialComs.initializePort()
        if result <> "success":
            tkMessageBox.showerror("Serial Communication Error", result)
            self.serialComs.closePort()
        else:
            self._microcontoller_connected = 1
            result = self._reconfigure_microcontroller()
            if result <> "success":
                self._microcontoller_connected = 0
                self.serialComs.closePort()
                tkMessageBox.showerror("Micro-controller Error", "There was an error configuring the micro-controller.\n"+ result)
                return
            
            tkMessageBox.showinfo("Serial Communication Info", "Serial Connection Established")
            self.MicroControllerMenu.delete(1)
            self.MicroControllerMenu.add_command(label = "Disconnect", command = self._disconnectMicro)
            self.MicroControllerMenu.update()
            
        
        return
    
    def _disconnectMicro(self):
        '''
        Function: _disconnectMicro
        
        Description:
            Shuts down all serial communications.
            
        Returns:
            Nothing
        '''
        result = self.serialComs.closePort()
        if result <> "success":
            tkMessageBox.showerror("Serial Communication Error", result)
        else:
            tkMessageBox.showinfo("Serial Communication Info", "Serial Connection Closed")
            self.MicroControllerMenu.delete(1)
            self.MicroControllerMenu.add_command(label = "Connect", command = self._connectMicro)
            self.MicroControllerMenu.update()
            self._microcontoller_connected = 0
        return
    
    def _get_file_path_csv(self):
        '''
        Function: _get_file_path_csv
        
        Description:
            Brings up a file browsing dialog for .csv files.
            
        Returns:
            file object that has been already opened
        '''
        file_opt = options = {}
        options['defaultextension'] = '.csv' # couldn't figure out how this works
        options['filetypes'] = [('CSV file', '.csv')]
        options['initialdir'] = ''
        options['initialfile'] = 'OSEMeasurementFile.csv'
        #options['parent'] = root
        options['title'] = 'OSEKits Measurement Saving'
        return  tkFileDialog.asksaveasfile(mode='w', **file_opt)
    
    def _get_file_path_xml(self):
        '''
        Function: _get_file_path_xml

        Description:
            Brings up a file browsing dialog for .xml files.
            
        Returns:
            file object that has been already opened
        
        '''
        file_opt = options = {}
        options['defaultextension'] = '.xml' # couldn't figure out how this works
        options['filetypes'] = [('XML file', '.xml')]
        options['initialdir'] = ''
        options['initialfile'] = 'OSEMeasurementFile.xml'
        #options['parent'] = root
        options['title'] = 'OSEKits Measurement Saving'
        return tkFileDialog.asksaveasfile(mode='w', **file_opt)
        
    
    def _save_measurements_csv(self):
        '''
        Function: _save_measurements_csv
        
        Description:
            Called when measurement data from the last experiment done is to be saved as a csv file. 
            
        Returns:
            Nothing
        '''
        if self.__sensorMode == OSEKitsDefine.THERMOCOUPLE_MODE:
            result = self._thermocouple.save_data('csv', self._get_file_path_csv())
        elif self.__sensorMode == OSEKitsDefine.ENCODER_MODE:
            result = self._encoder.save_data('csv', self._get_file_path_csv())
        else:
            #tkMessageBox.showerror("Data Export Error", "\nData Export is not available for this sensor mode.")
            #result = "success"
            result = "Data Export is not available for this sensor mode."
        if result <> "success":
            tkMessageBox.showerror("File I/O Error", result)
        
        return

    def _save_measurements_xml(self):
        '''
        Function: _save_measurements_xml
        
        Description:
            Called when measurement data from the last experiment done is to be saved as a xml file. 
            
        Returns:
            Nothing

        '''
 
        if self.__sensorMode == OSEKitsDefine.THERMOCOUPLE_MODE:
            result = self._thermocouple.save_data('xml', self._get_file_path_xml())
        elif self.__sensorMode == OSEKitsDefine.ENCODER_MODE:
            result = self._encoder.save_data('xml', self._get_file_path_xml())
        else:
            #tkMessageBox.showerror("Data Export Error", "\nData Export is not available for this sensor mode.")
            #result = "success"        
            result = "Data Export is not available for this sensor mode."
        if result <> "success":
                tkMessageBox.showerror("File I/O Error", result)
        
        return
                
                
    def _createMenus(self):
        '''
        Function: _createMenus
        
        Description:
            Private function to add the main menus to the top of the OSEKits application
            window.
        
        Returns:
            Nothing
        '''
        # create the main drop down menu bar
        MainMenu = Menu(self.root_window)
        self.root_window.config(menu=MainMenu)
        
        #create the File pull down menu
        filemenu = Menu(MainMenu, tearoff=0)
        MainMenu.add_cascade(label="File", menu=filemenu)
        filemenu.add_command(label="Save Measurements (csv)", command=self._save_measurements_csv)
        filemenu.add_command(label="Save Measurements (xml)", command=self._save_measurements_xml)
        filemenu.add_command(label="Exit", command=self._terminate)
        
        #create the Microcontroller pull down menu
        MicroControllermenu = self.MicroControllerMenu = Menu(MainMenu, tearoff=0)
        MainMenu.add_cascade(label="Micro-Contoller", menu=MicroControllermenu)
        MicroControllermenu.add_command(label="Settings", command=self._microControllerSettings)
        MicroControllermenu.add_command(label="Connect", command=self._connectMicro)
        
        #create the Sensor pull down menu
        SensorMenu = self.SensorMenu = Menu(MainMenu, tearoff=0)
        MainMenu.add_cascade(label = 'Sensor', menu=SensorMenu)
        SensorMenu.add_command(label = "Thermocouple", command = self._reconfigure_To_Thermocouple)
        SensorMenu.add_command(label = "Encoder", command = self._reconfigure_To_Encoder)
        SensorMenu.add_command(label = "Photogate", command = self._reconfigure_To_Photogate)
        
        helpmenu = Menu(MainMenu, tearoff=0)
        MainMenu.add_cascade(label="Help", menu=helpmenu)
        helpmenu.add_command(label="About...", command=self._aboutDisplay)
        
        return()
    
    def _reconfigure_microcontroller(self):
        '''
        Function: _reconfigure_microcontroller
        
        Description: 
            This function is called to send configuration data for the current sensor to the micro-controller.
        
        Returns:
            string  "success" if no errors
                    "<error message:>" otherwise
                
        '''
        if self._microcontoller_connected == 1:
            configdata = []
          
            if self.__sensorMode == OSEKitsDefine.PHOTOGATE_MODE:
                if self._photogate._number_of_photogates == 2:
                    configdata.append(0x02)
                elif self._photogate._number_of_photogates == 3:
                    configdata.append(0x03)
                elif self._photogate._number_of_photogates == 4:
                    configdata.append(0x04)
                elif self._photogate._number_of_photogates == 5:
                    configdata.append(0x05)
                elif self._photogate._number_of_photogates == 6:
                    configdata.append(0x06)
                elif self._photogate._number_of_photogates == 7:
                    configdata.append(0x07)
                elif self._photogate._number_of_photogates == 8:
                    configdata.append(0x08)
            return self.serialComs.sendConfigPacket(self.__sensorMode, configdata)
        else:
            return "success"
        
    def _reconfigure_display(self, new_sensor_mode):
        '''
        Function: _reconfigure_display
        
        Description:
            Called to switch the display from one sensor mode to another.
            
        Returns:
            Nothing
        '''
        #check to make sure experiment is not running
        if self.__measurementFlag <> 2:
            tkMessageBox.showerror("Sensor Switch Error", "Cannot Switch Sensor types while Experiment is running.")
            return
        #check if switching to current sensor mode
        if new_sensor_mode == self.__sensorMode:
            return
        
        #destroy the current window display
        if self.__sensorMode == OSEKitsDefine.THERMOCOUPLE_MODE:
            self._thermocouple.destroy_display(self.root_window)
            
        elif self.__sensorMode == OSEKitsDefine.ENCODER_MODE:
            self._encoder.destroy_display(self.root_window)

        elif self.__sensorMode == OSEKitsDefine.PHOTOGATE_MODE:
            self._photogate.destroy_display(self.root_window)

        #create the new window display
        if new_sensor_mode == OSEKitsDefine.THERMOCOUPLE_MODE:
            self._thermocouple.setup_display(self.root_window)

        elif new_sensor_mode == OSEKitsDefine.ENCODER_MODE:
            self._encoder.setup_display(self.root_window)

        elif new_sensor_mode == OSEKitsDefine.PHOTOGATE_MODE:
            self._photogate.setup_display(self.root_window, self._startstopButton)

        else:
            return
        
        #set the new sensor mode as current
        self.__sensorMode = new_sensor_mode
        
        #move the start/stop button
        self._startstopButton.grid(column = 0, row = OSEKitsDefine.CONTROL_ROW, sticky =S+W)
        
        #send new configuration data to the micro-controller
        result = self._reconfigure_microcontroller()
        if result <> "success":
            tkMessageBox.showerror("Micro-controller Error", "There was an error configuring the micro-controller.\n" + result)
        
        return
    
    def _reconfigure_To_Thermocouple(self):
        '''
        Function: _reconfigure_To_Thermocouple
        
        Description:
            Configures the display and micro-controller to use the thermocouple.
        
        Returns:
            Nothing
        '''
        self._reconfigure_display(OSEKitsDefine.THERMOCOUPLE_MODE)
        return
    def _reconfigure_To_Encoder(self):
        '''
        Function: _reconfigure_To_Encoder
        
        Description:
            Configures the display and micro-controller to use the Encoder.
        
        Returns:
            Nothing
        '''
        self._reconfigure_display(OSEKitsDefine.ENCODER_MODE)
        return
    def _reconfigure_To_Photogate(self):
        '''
        Function: _reconfigure_To_Photogate
        
        Description:
            Configures the display and micro-controller to use the photogate.
        
        Returns:
            Nothing
        '''

        self._reconfigure_display(OSEKitsDefine.PHOTOGATE_MODE)
        return
